/* Copyright (c) 2009 Google Inc.
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *     http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.jessies.blogbaboon;

import com.google.gdata.data.*;
import com.google.gdata.data.blogger.*;
import e.forms.*;
import e.gui.*;
import e.util.*;
import e.ptextarea.*;
import java.awt.*;
import java.awt.event.*;
import java.io.*;
import java.net.*;
import javax.swing.*;
import javax.swing.event.*;

public class EditingWindow extends JFrame {
  private final BlogBaboon blogEditor;
  private final PostEntry post;
  
  private final JTextField titleField = new JTextField(40);
  private String originalTitle;
  private final PTextArea textArea = new PTextArea(20, 40);
  private final EStatusBar statusBar = new EStatusBar();
  
  public final Action closeAction;
  public final Action previewAction;
  private final Action publishAction;
  public final Action saveDraftAction;
  
  // We cache the template because network access is potentially expensive and the template's unlikely to change.
  private String cachedTemplate;
  // We use the same file as long as you're editing the post, to encourage the browser to reuse a window.
  private File previewFile;
  
  public EditingWindow(BlogBaboon blogEditor, PostEntry post) {
    this.blogEditor = blogEditor;
    this.post = post;
    this.originalTitle = post.getTitle().getPlainText();
    
    this.closeAction = new CloseAction(this);
    this.previewAction = new PreviewAction(this);
    this.publishAction = new PublishAction(this);
    this.saveDraftAction = new SaveDraftAction(this);
    
    titleField.setText(originalTitle);
    titleField.setCaretPosition(0);
    titleField.setEditable(post.isDraft());
    titleField.addFocusListener(blogEditor.actionStateUpdater);
    // Keep the title bar up-to-date with respect to the title field.
    titleField.getDocument().addDocumentListener(new DocumentAdapter() {
      public void documentChanged() { updateTitleBar(); }
    });
    // Set up the invariant.
    updateTitleBar();
    
    textArea.setText(decodeContent());
    textArea.getTextBuffer().getUndoBuffer().resetUndoBuffer();
    textArea.getTextBuffer().getUndoBuffer().setCurrentStateClean();
    textArea.setWrapStyleWord(true);
    textArea.setTextStyler(new PXmlTextStyler(textArea));
    textArea.getTextBuffer().addTextListener(new PTextListener() {
      public void textInserted(PTextEvent event) { textChanged(); }
      public void textRemoved(PTextEvent event) { textChanged(); }
      public void textCompletelyReplaced(PTextEvent event) { textChanged(); }
    });
    textArea.addFocusListener(blogEditor.actionStateUpdater);
    
    preferencesChanged();
    
    setLayout(new BorderLayout());
    setJMenuBar(new BlogBaboonMenuBar(blogEditor, this));
    
    final JScrollPane scrollableText = new JScrollPane(textArea, JScrollPane.VERTICAL_SCROLLBAR_ALWAYS, JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
    if (GuiUtilities.isMacOs()) {
      // On Mac OS, the heavy focus ring looks silly.
      // Replace it with something that makes us look like Text Edit.
      scrollableText.setBorder(BorderFactory.createMatteBorder(1, 0, 0, 0, UIManager.getColor("TabbedPane.shadow")));
    }
    
    // Put together the overall UI.
    add(makeTopPanel(), BorderLayout.NORTH);
    add(scrollableText, BorderLayout.CENTER);
    add(statusBar, BorderLayout.SOUTH);
    pack();
    
    initCloseBehavior();
    initFocus();
    
    setLocationRelativeTo(blogEditor);
    
    previewAction.setEnabled(true);
    publishAction.setEnabled(post.isDraft());
    saveDraftAction.setEnabled(false);
    
    JFrameUtilities.setFrameIcon(this);
    setVisible(true);
    
    InstanceTracker.addInstance(this);
  }
  
  private void updateTitleBar() {
    textChanged();
    post.setTitle(new HtmlTextConstruct(titleField.getText()));
    setTitle(BlogUtils.displayableTitle(post));
  }
  
  private void initCloseBehavior() {
    // Disable the default close operation and add our own WindowListener.
    setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
    addWindowListener(new WindowAdapter() {
      @Override public void windowClosing(WindowEvent e) {
        maybeClose();
      }
    });
    // Mac OS uses command-W to close a window using the keyboard.
    // Unlike Linux and Windows' alt-f4, though, this isn't done by the window manager.
    if (GuiUtilities.isMacOs()) {
      final KeyStroke commandW = KeyStroke.getKeyStroke(KeyEvent.VK_W, InputEvent.META_MASK, false);
      JFrameUtilities.closeOnKeyStroke(this, commandW);
    }
  }
  
  public boolean maybeClose() {
    if (!isDirty() || SimpleDialog.askQuestion(this, "Discard Changes", "Discard changes to post \"" + BlogUtils.displayableTitle(post) + "\"?", "Discard")) {
      dispose();
      return true;
    }
    return false;
  }
  
  private void initFocus() {
    titleField.addActionListener(new ActionListener() {
      public void actionPerformed(ActionEvent e) {
        textArea.requestFocusInWindow();
      }
    });
    if (titleField.getText().length() == 0) {
      titleField.requestFocusInWindow();
    } else {
      textArea.requestFocusInWindow();
    }
  }
  
  private JComponent makeTopPanel() {
    JPanel topPanel = new JPanel(new BorderLayout());
    topPanel.setBorder(BorderFactory.createEmptyBorder(0, 0, 6, 0));
    topPanel.add(makeToolBar(), BorderLayout.NORTH);
    topPanel.add(makeHeaderForm(), BorderLayout.CENTER);
    return topPanel;
  }
  
  private JComponent makeHeaderForm() {
    FormPanel headerForm = new FormPanel();
    headerForm.addRow("Title:", titleField);
    // FIXME: more fields?
    return headerForm;
  }
  
  private JToolBar makeToolBar() {
    if (post.isDraft()) {
      // Only add a "Publish" button if the post isn't already published.
      return UiUtils.makeToolBar(saveDraftAction, previewAction, publishAction);
    } else {
      return UiUtils.makeToolBar(saveDraftAction, previewAction);
    }
  }
  
  public boolean isDirty() {
    return !originalTitle.equals(titleField.getText()) || !textArea.getTextBuffer().getUndoBuffer().isClean();
  }
  
  private void textChanged() {
    final boolean isDirty = isDirty();
    // For toolbar users...
    blogEditor.updateActionEnabledState();
    // For Mac OS 10.4 users...
    getRootPane().putClientProperty("windowModified", isDirty);
    // For Mac OS 10.5 users...
    getRootPane().putClientProperty("Window.documentModified", isDirty);
    
    saveDraftAction.setEnabled(isDirty);
  }
  
  public void preferencesChanged() {
    final Preferences preferences = blogEditor.getPreferences();
    textArea.setFont(preferences.getFont(BlogBaboonPreferences.EDITOR_FONT));
    textArea.setShouldHideMouseWhenTyping(preferences.getBoolean(BlogBaboonPreferences.HIDE_MOUSE_WHEN_TYPING));
  }
  
  private String decodeContent() {
    final TextConstruct contentConstruct = post.getTextContent().getContent();
    // FIXME: do we ever see either of the other two TextConstruct subclasses?
    String content = ((HtmlTextConstruct) contentConstruct).getHtml();
    return new WikiCodec().fromHtml(content);
  }
  
  private String encodeContent() {
    return new WikiCodec().toHtml(textArea.getText());
  }
  
  private String retrieveTemplate() throws Exception {
    if (cachedTemplate == null) {
      final URL url = new URL(blogEditor.getBlogInfo().getBlogUrl());
      final BufferedReader in = new BufferedReader(new InputStreamReader(url.openStream()));
      final StringBuilder result = new StringBuilder();
      String line;
      while ((line = in.readLine()) != null) {
        result.append(line);
      }
      in.close();
      cachedTemplate = result.toString();
    }
    return cachedTemplate;
  }
  
  public void preview() {
    try {
      // FIXME: is this the structure all posts have?
      String postContent = "";
      // FIXME: infer the user's chosen date format from the template page we read?
      postContent += "<h2 class='date-header'>" + post.getPublished().toUiString() + "</h2>\n";
      postContent += "<div class='post hentry'>";
      postContent += " <h3 class='post-title entry-title'>";
      postContent += "  <a href=''>" + BlogUtils.displayableTitle(post) + "</a>";
      postContent += " </h3>\n";
      postContent += " <div class='post-header-line-1'></div>";
      postContent += " <div class='post-body entry-content'>\n";
      postContent += encodeContent() + "\n";
      postContent += "  <div style='clear: both;'></div>";
      postContent += " </div>";
      postContent += " <div class='post-footer'>";
      postContent += "  <div class='post-footer-line post-footer-line-1'>";
      postContent += "   <span class='post-icons'>";
      postContent += "   </span>";
      postContent += "   <span class='post-labels'>";
      postContent += "   </span>";
      postContent += "  </div>";
      postContent += "  <div class='post-footer-line post-footer-line-2'></div>";
      postContent += "  <div class='post-footer-line post-footer-line-3'></div>";
      postContent += " </div>";
      postContent += "</div>\n";
      
      final String preview = retrieveTemplate().replaceAll("<!-- google_ad_section_start.*<!-- google_ad_section_end -->", StringUtilities.replacementStringFromLiteral(postContent));
      
      // At least on Mac OS with Camino, opening the same URL again just causes a reload in the existing window.
      // FIXME: test other browsers and OSes.
      // FIXME: look at working with a native HTML view. (Swing's one is useless. I tried that first.)
      if (previewFile == null) {
        final String fileDescription = "Preview of " + BlogUtils.displayableTitle(post);
        previewFile = FileUtilities.createTemporaryFile("preview-", ".html", fileDescription, preview);
      } else {
        StringUtilities.writeFile(previewFile, preview);
      }
      BrowserLauncher.openURL(previewFile.toURI().toString());
    } catch (Exception ex) {
      SimpleDialog.showDetails(this, "Failed to show preview.", ex);
    }
  }
  
  public void publish() {
    if (!SimpleDialog.askQuestion(this, "Publish Post?", "Publish the post \"" + BlogUtils.displayableTitle(post) + "\"?", "Publish")) {
      return;
    }
    saveOrPublish(true);
  }
  
  public void saveDraft() {
    saveOrPublish(false);
  }
  
  private void saveOrPublish(boolean shouldPublish) {
    post.setTitle(new HtmlTextConstruct(titleField.getText()));
    post.setContent(new HtmlTextConstruct(encodeContent()));
    post.setUpdated(DateTime.now());
    if (shouldPublish) {
      post.setDraft(false);
      post.setPublished(DateTime.now());
    }
    try {
      // Try to save/publish.
      post.update();
      blogEditor.updatePostList();
      
      // If we get here, the changes have been committed.
      originalTitle = post.getTitle().getPlainText();
      textArea.getTextBuffer().getUndoBuffer().setCurrentStateClean();
      textChanged();
      publishAction.setEnabled(post.isDraft());
      
      // If we just published, close this window.
      if (shouldPublish) {
        maybeClose();
      }
    } catch (Exception ex) {
      // FIXME: just report in the status bar? distinguish between user-initiated and timer saves?
      SimpleDialog.showDetails(this, shouldPublish ? "Failed to publish post." : "Failed to save draft.", ex);
    }
  }
}
